package serverClient;

import sync.Checksum;
import sync.FileInfo;
import sync.FileList;
import gui.GuiTreeSelectFiles;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.Semaphore;

public class Server extends ServerClient {
	
	private final boolean DEBUG = false;
	
	// Variables for the checksum thread
	private Thread checksumThread; // The thread that counts all the checksums
	private Semaphore startChecksumSemaphore; // Semaphore to start the count of one checksum
	private Semaphore checksumDoneSemaphore; // Semaphore to mark that a checksum is ready
	private boolean allChecksumsDone; // Boolean to finish the checksum thread when all checksums are done
	private FileInfo tempFileInfoChecksumThread; // Variable for the checksum thread of the current fileInfo
	private long incommingChecksumThread; // Variable for the checksum thread of the current checksum value
	private boolean threadThrowsException = false; // Boolean to interrupt execution if checksum thread throws exception
	
	private boolean setFileAttributeHidden = false; // Is true if we are in Windows and we include hidden files 
	
	
	public Server(String confDir, BufferedInputStream inBufStream, BufferedOutputStream outBufStream, File workDir, boolean allowDelete, boolean deleteWarning, boolean includeHidden,  String syncMode, boolean useExactTime) {
		
		super(confDir, inBufStream, outBufStream, workDir, allowDelete, deleteWarning, includeHidden, syncMode, useExactTime);
		
		// Check if we have the special case where we need to take care of hidden files in a Windows system
		if(includeHidden) {
			String os = System.getProperty("os.name").toUpperCase();
			if (os.indexOf("WIN") >= 0) {
				setFileAttributeHidden = true;
			}
		}
	}
	
	
	// Runs the server
	// Calls different methods to go through the protocol
	public boolean runServer() {
		
		boolean ranSuccessfully = true; // Is the return value of the function
		try {
			if(!isSecondSyncThread) sendTranferMessage("Gathering data. This may take several minutes..."); // Sending message to observer
			FileList ourFL = createOurFileList();
			sendOurFileList();
			FileList remoteFL = receiveRemoteFileList();
			boolean isSynced = checkIfSynced(ourFL, remoteFL);
			if(!isSynced) {
				remoteFL = receiveRemoteFileList();
				deleteTheOldTransferFileList();
				FileList transferFL = receiveTransferFileList();
				sendInfoToGui(transferFL);
				receiveFiles(transferFL);
				if(!isSecondSyncThread) sendTranferMessage("Gathering data..."); // Sending message to observer
				ourFL = mergeOurFileLists(ourFL, transferFL);
				if(allowDelete) { // Runs this if delete is allowed
					FileList deleteFL;
					if(syncMode.equals("backup")) {
						deleteFL = createDeleteFileListInBackupMode(ourFL, remoteFL);
					}
					else {
						deleteFL = createDeleteFileList(remoteFL);
						deleteFL = handleDeleteFileList(deleteFL, ourFL);
					}
					if(deleteWarning && deleteFL.getSize() > 0) {
						GuiTreeSelectFiles selectFiles = new GuiTreeSelectFiles(null, true, deleteFL, workDir.getPath());
						deleteFL = selectFiles.toDelete();
						//deleteFL.saveToTextFile(confDir + DELETE_FL + fileNameExtension + ".filel");
					}
					deleteFiles(deleteFL, workDir.getAbsolutePath(), "");
					ourFL = handleOurFileList(ourFL, deleteFL);
				}
				sendOurFileList();
			}
			saveOldRemoteFileList(remoteFL);
			deleteUnneededFileLists();
			if(printFinalSyncCheck) finalSyncCheck(isSynced, ourFL, remoteFL);
			
		} catch (Exception e) {
			ranSuccessfully = false;
			e.printStackTrace();
			if(errorMessage.equals("")) {
				sendConnectionWarning("File transfer failed!");
			}
			else {
				sendConnectionWarning(errorMessage);
			}
		}
		
		closeBuffs();
		return ranSuccessfully;
	}
	
	
	// Merges two fileLists 
	// Is used after files have been received to update the local FileList
	private FileList mergeOurFileLists(FileList ourFL, FileList transferFL) throws Exception {
		
		FileList newFL;
		if(syncMode.equals("backup")) {
			newFL = ourFL.mergeFileLists(transferFL, true);
		}
		else {
			newFL = ourFL.mergeFileLists(transferFL, false);
		}
		
		try {
			newFL.saveToTextFile(confDir + LOCAL_FL + fileNameExtension + ".filel");
		} catch (FileNotFoundException e) {
			errorMessage = "Could not store a needed data file!";
			throw new Exception();
		} catch (Exception e) {
			errorMessage = "System can not write a needed file!";
			throw new Exception();
		}
		
		return newFL;
	}
	
	// Function to create a deleteList in backup mode
	// This will return a FL of files that the server has but not the client
	private FileList createDeleteFileListInBackupMode(FileList ourFL, FileList remoteFL) {

		FileList deleteList;

		deleteList = ourFL.compareFileListNamesOnly(remoteFL);
		
		//deleteList.saveToTextFile(confDir + DELETE_FL + fileNameExtension + ".filel");

		return deleteList;
	}
	
	// Method that receives all the the files from the transferFileList to the client
	// and creates directory's as needed
	private void receiveFiles(FileList filesToRecieve) throws Exception {
		
		// Amount of files to be received plus directory's
		int noOfFiles = filesToRecieve.getSize();
		FileInfo tempFileInfo;
		
		// Starts the thread that counts the checksums
		allChecksumsDone = false;
		startChecksumThread();
		
		for(int i=0; i < noOfFiles; i++) {
			if(!isSecondSyncThread) fileInProgress(); // Is for updating which file is being processed to the progress bar
			
			tempFileInfo = filesToRecieve.get(i);
			//Directory control
			String fileName = tempFileInfo.getName();
			char dirSymbol = fileName.charAt(fileName.length()-1); // Gets the last symbol
			// Creates a map
			if(dirSymbol == '/') {
				if(!isSecondSyncThread) sendTranferMessage("Create dir: " + fileName); // Sending message to observer
				if(isSecondSyncThread) sendTranferMessage("2:nd create dir: " + fileName); // Sending message to observer
				createDir(tempFileInfo);
			}
			// Receives a file
			else {
				if(!isSecondSyncThread) sendTranferMessage("Receive: " + fileName); // Sending message to observer
				receiveFile(getFilePath(tempFileInfo), tempFileInfo);
				checksumDoneSemaphore.acquire(); // Special case the first time
				if(threadThrowsException) {
					throw new Exception(); // Interrupt execution if previous checksum did fail
				}
				incommingChecksumThread = receiveChecksum();
				tempFileInfoChecksumThread = tempFileInfo; // A copy to the checksum thread
				startChecksumSemaphore.release();
			}
		}
		
		// End the thread that counts the checksums
		checksumDoneSemaphore.acquire(); // Special case the last time
		allChecksumsDone = true;
		startChecksumSemaphore.release(); // Special case the last time
		checksumThread.join();
		if(DEBUG) System.out.println("Is checksumThread alive? " + checksumThread.isAlive());
		
		if(!isSecondSyncThread) filesTransferFinished(); // send info to guiStatusBar
	}
	
	// Start a thread that count all checksums
	private void startChecksumThread() {
		
		// Initiates the needed semaphores
		startChecksumSemaphore = new Semaphore(0);
		checksumDoneSemaphore = new Semaphore(1);
		
		checksumThread = new Thread(new Runnable() {
			public void run() {
				while(!allChecksumsDone) {
					try {
						startChecksumSemaphore.acquire();
						if(allChecksumsDone) {
							break;
						}
						// Figures out in the checksums are equal
						compareCheckSum(getFilePath(tempFileInfoChecksumThread), incommingChecksumThread);
						// Saves the file in the right way
						handleReceivedFile(tempFileInfoChecksumThread);
					} catch (InterruptedException e) {
						threadThrowsException = true;
						if(DEBUG) System.err.println("Problems with the startChecksum semaphore!");
					} catch (Exception e) {
						threadThrowsException = true;
						if(DEBUG) System.err.println("Could not make checksum or rename the file!");
					}
					checksumDoneSemaphore.release();
				}
			}
		});
		checksumThread.start();
	}
	
	// Receives one file through a buffer that is
	private void receiveFile(String path, FileInfo temp) throws Exception {
		
		if(DEBUG) System.out.println("Receiving: " + path);
		
		receive(path + ".partSync", temp.getFileLength());
	}
	
	private long receiveChecksum() throws Exception {
		
		// Receives the checksum
		byte[] longByte = new byte[8];
		try {
			receiveBytesOfLong(longByte, 0, 8); // recursive function call
		} catch (Exception e) {
			errorMessage = "Could not receive checksum!";
			throw new Exception();
		}
		long checksum = byteArrToLong(longByte);
		return checksum;
	}
	
	// Make an own check sum and compare it with the received one
	private void compareCheckSum(String path, long incommingChecksum) throws Exception {
		
		boolean checks;
		try {
			checks = Checksum.compateChecksum(path + ".partSync", incommingChecksum);
		} catch (Exception e) {
			errorMessage = "Could not calculate checksum!";
			throw new Exception();
		}
		if(checks) {
			if(DEBUG) System.out.println("Checksum match!");
		}
		else {
			if(DEBUG) System.out.println("Checksum check failed!");
			errorMessage = "Checksums do not match!";
			throw new Exception();
		}
	}
	
	private void handleReceivedFile(FileInfo tempFileInfoChecksumThread) throws Exception {
		
		// Creates variables that are needed
		String path = getFilePath(tempFileInfoChecksumThread);
		long modTime = tempFileInfoChecksumThread.getModTime();
		int windowsHiddenChecksumThread = tempFileInfoChecksumThread.getWindowsHidden();
		File saveFile = new File(path);
		File tempFile = new File(path + ".partSync");
		
		if(saveFile.exists()) {
			if(!saveFile.delete()) { // Deletes the old file
				errorMessage = "Could not replace the old file " + saveFile.getAbsolutePath() + " !";
				throw new Exception();
			}
		} 
		
		if(DEBUG) System.out.println("Saving received file");
		if (!tempFile.renameTo(saveFile)) {
			if(DEBUG) System.out.println("Rename Failed!");
		}
		
		// Change modTime for the saved file
		if(DEBUG) System.out.println("For file: " + path + ", set mod time to: " + modTime);
		saveFile.setLastModified(modTime);
		long actualModTime = saveFile.lastModified(); // Some file systems round up the modTime
		if(modTime != actualModTime) { // If the system modified the modTime. Also change it in the fileList
			if(DEBUG) System.out.println("Change modTime form " + modTime + " to " + actualModTime);
			tempFileInfoChecksumThread.setModTime(actualModTime);
		}
		
		// Set the file attribute to hidden if we are in Windows and if the file should be hidden
		if(DEBUG) System.out.println("File: " + path + ", is windows hidden: " + windowsHiddenChecksumThread);
		setHiddenAttribute(windowsHiddenChecksumThread, path);
	}
	
	// Creates a directory as directed from the transferFileList
	private void createDir(FileInfo temp) throws Exception {
		
		// Creates variables that are needed
		String dirName = temp.getName();
		// Takes away the last char '/'
		dirName = dirName.substring(0, dirName.length()-1);
		// Changes the separator character to the system-dependent default name-separator character
		dirName = dirName.replace('/', File.separatorChar);
		String path = workDir.getPath() + File.separator + dirName;
		
		if(DEBUG) System.out.println("Creating dir: " + path + File.separator);
		
		// Creates the directory
		File newDir = new File(path);
		if(!newDir.exists()) {
			newDir.mkdir();
		}
		
		// If hidden directory and in Windows, set the hidden attribute of the file
		setHiddenAttribute(temp.getWindowsHidden(), path);
	}

	// Set the file attribute to hidden if we are in Windows and if the file should be hidden
	private void setHiddenAttribute(int windowsHidden, String path) throws Exception {
		if( setFileAttributeHidden && (windowsHidden == 1) ) {
			try {
				Runtime.getRuntime().exec( "C:\\WINDOWS\\System32\\ATTRIB.EXE +H \"" + path + "\"" );
			} catch (IOException e) {
				errorMessage = "Could not hide the file " + path + " !";
				throw new Exception();
			}
		}
	}
	
}
